package com.github.alexvictoor.proxy;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.List;
import static io.netty.handler.codec.http.HttpHeaders.Names.*;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
public class HttpFrontEndHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private static final Logger logger = LoggerFactory.getLogger(HttpFrontEndHandler.class);
private final String host;
private final int port;
private final List<FileSystemRoute> routes;
private ChannelFuture channelFuture;
public HttpFrontEndHandler(String host, int port, List<FileSystemRoute> routes) {
this.host = host;
this.port = port;
this.routes = routes;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, final FullHttpRequest msg) throws Exception {
final String uri = msg.getUri().substring(1); // remove first '/'
logger.debug("REQ URI {}", uri);
for (FileSystemRoute route : routes) {
File file = route.findFile(uri);
if (file != null) {
handleFileRequest(ctx, msg, file);
return;
}
}
handleProxyRequest(ctx, msg);
}
private void handleProxyRequest(final ChannelHandlerContext ctx, final FullHttpRequest msg) {
final Channel inboundChannel = ctx.channel();
// Start the connection attempt.
Bootstrap b = new Bootstrap();
ChannelInitializer<SocketChannel> initializer = new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new HttpClientCodec());
// Remove the following line if you don't want automatic content decompression.
//p.addLast(new HttpContentDecompressor());
p.addLast(new HttpObjectAggregator(1048576));
p.addLast(new HttpBackEndHandler(inboundChannel));
}
};
b.group(inboundChannel.eventLoop())
.channel(NioSocketChannel.class)
.handler(initializer);
// Make the connection attempt.
if (channelFuture == null || (channelFuture.isSuccess() && !channelFuture.channel().isOpen())) {
logger.debug("Instantiating new connection");
channelFuture = b.connect(host, port);
} else {
logger.debug("Reusing connection");
}
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
// Prepare the HTTP request.
HttpRequest request = new DefaultFullHttpRequest(
HttpVersion.HTTP_1_1, msg.getMethod(), msg.getUri());
request.headers().add(msg.headers());
request.headers().set(HttpHeaders.Names.HOST, host);
//request.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
//request.headers().set(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.GZIP);
future.channel().writeAndFlush(request);
} else {
logger.info("Connection issue", future.cause());
sendError(ctx, HttpResponseStatus.BAD_GATEWAY);
}
}
});
}
private void handleFileRequest(ChannelHandlerContext ctx, FullHttpRequest msg, File file) throws IOException {
if (msg.getMethod() != GET) {
sendError(ctx, METHOD_NOT_ALLOWED);
return;
}
RandomAccessFile raf;
try {
raf = new RandomAccessFile(file, "r");
} catch (FileNotFoundException ignore) {
sendError(ctx, NOT_FOUND);
return;
}
long fileLength = raf.length();
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
HttpHeaders.setContentLength(response, fileLength);
setContentTypeHeader(response, file);
if (HttpHeaders.isKeepAlive(msg)) {
response.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
}
// Write the initial line and the header.
ctx.write(response);
// Write the content.
ctx.write(new DefaultFileRegion(raf.getChannel(), 0, fileLength), ctx.newProgressivePromise());
// Write the end marker
ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
// Decide whether to close the connection or not.
if (!HttpHeaders.isKeepAlive(msg)) {
// Close the connection when the whole content is written out.
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}
private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8));
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
// Close the connection as soon as the error message is sent.
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private void setContentTypeHeader(HttpResponse response, File file) {
MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
response.headers().set(CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath()));
}
}